<?php
/**
 * Created by 784855684@qq.com.
 * Link: https://github.com/lichtung/wshore
 * User: linzh
 * Date: 6/25/17
 * Time: 5:45 PM
 */
declare(strict_types=1);


namespace wshore\core;

use wshore\Component;
use wshore\Developer;
use wshore\helper\Chars;
use wshore\RouteInterface;

class Router extends Component implements RouteInterface
{

    protected $_config = [

        //------------------------
        //For URL route
        //------------------------
        'route_on' => true,//总开关,是否对URI地址进行路由
        'static_route_on' => true,
        //静态路由规则
        'static_route_rules' => [],
        'wildcard_route_on' => false,
        //通配符路由规则,具体参考CodeIgniter
        'wildcard_route_rules' => [],
        'regular_route_on' => true,
        //正则表达式 规则
        'regular_route_rules' => [],

        //------------------------
        //For URL parser
        //------------------------
        //API模式，直接使用$_GET
        'api_mode_on' => false,
        //API模式 对应的$_GET变量名称
        'api_modules_variable' => '_m',
        'api_controller_variable' => '_c',
        'api_action_variable' => '_a',

        //普通模式
        'masquerade_tail' => '.html',
        //*** 必须保证操作与控制器之间的符号将是$_SERVER['PATH_INFO']字符串中第一个出现的,为了更好地显示URL，参数一般通过POST传递
        //特别注意的是若使用了问号，则后面的字符串将被认为是请求参数
        'ap_bridge' => '.',

        //是否开启域名部署（包括子域名部署）
        'domain_deploy_on' => false,
        //子域名部署模式下 的 完整域名
        'domain' => '',
        //是否将子域名段和模块进行映射
        'subdomain_auto_mapping_on' => false,
        //子域名部署规则
        //注意参与array_flip()函数,键值互换
        'subdomain_mappinig' => [],
    ];

    /**
     * 获取pathinfo信息
     *
     * 1.http://localhost/litera/public/
     * 'REQUEST_URI' => '/litera/public/',
     * 'SCRIPT_NAME' => '/litera/public/index.php',
     *
     *
     * @return string
     */
    public function getPathInfo(): string
    {
        $url = null;

        if (isset($_SERVER['PATH_INFO'])) {
            //如果设置了PATH_INFO则直接获取之
            $url = $_SERVER['PATH_INFO'];
        } else {
            $scriptlen = strlen($_SERVER['SCRIPT_NAME']);

            # 去除参数部分
            if (strpos($_SERVER['REQUEST_URI'], '?') !== false) {
                $request_url = strstr($_SERVER['REQUEST_URI'], '?', true);
            } else {
                $request_url = $_SERVER['REQUEST_URI'];
            }

            if (strlen($request_url) > $scriptlen) {
                $pos = strpos($request_url, $_SERVER['SCRIPT_NAME']);
                if (false !== $pos) {
                    //在不支持PATH_INFO...或者PATH_INFO不存在的情况下(URL省略将被认定为普通模式)
                    //REQUEST_URI获取原生的URL地址进行解析(返回脚本名称后面的部分)
                    if (0 === $pos) {//PATHINFO模式
                        $url = substr($request_url, $scriptlen);
                    } else {
                        //重写模式
                        $url = $request_url;
                    }
                } else {
                    $url = $request_url;
                }
            } else {
                if (strpos($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_URI']) === 0) {
                    # 符合情况1的URL
                } else {
                    # "/icode/r26" 和 '/index.php' 长度一致
                    $url = $request_url;
                }
            }
        }
        return $url ?? '/';
    }

//----------------------------------------- parse ---------------------------------//

    /**
     * 解析url
     * @param string $url 待解析的url
     * @return array 返回的格式如：['modules','controller','action']
     */
    public function parse(string $url): array
    {

        //路由开启的情况下进行路由解析
        if ($this->_config['route_on']) {
            //静态路由
            if ($this->_config['static_route_on'] and $static = $this->_config['static_route_rules']) {
                if (isset($static[$url])) {
                    $location = $static[$url];

                    if (is_string($location)) {
                        if (strpos($location, 'http') === 0) {
                            $_REQUEST and $location .= (strpos($location, '?') ? '&' : '?') . http_build_query($_REQUEST);
                            Response::redirect($location);
                        } else {
                            $location = self::parsePath($location);
                            $location[] = [];
                        }
                    }
                    return $location;
                }
            }
            //规则路由
            if ($this->_config['wildcard_route_on'] and $wildcard = $this->_config['wildcard_route_rules']) {
                foreach ($wildcard as $pattern => $rule) {
                    // Convert wildcards to RegEx（from CI）
                    //any对应非/的任何字符,num对应数字 ,id表示identify，即标识符(字母开头，只有下划线和字母)
                    $pattern = str_replace(['[any]', '[num]', '[id]'], ['([\w\d_]+)', '([0-9]+)', '(\w[\w_\d]?)'], $pattern);//$pattern = preg_replace('/\[.+?\]/','([^/\[\]]+)',$pattern);//非贪婪匹配
                    $result = self::matchRegular($pattern, $url, $rule);
                    if (is_array($result)) {
                        return $result;
                    } elseif (is_string($result)) {
                        return self::parsePath($result);
                    }
                }
            }
            //正则路由
            if ($this->_config['regular_route_on'] and $regular = $this->_config['regular_route_rules']) {
                foreach ($regular as $pattern => $rule) {
                    $result = self::matchRegular($pattern, $url, $rule);
                    if (null !== $result) return $result;
                }
            }
        }

        $result = [];

        //进行URL解析
        if ($this->_config['api_mode_on']) {
            //API模式下

            Developer::status('fetchurl_in_apimode_begin');

            $m = $this->_config['api_modules_variable'];
            $c = $this->_config['api_controller_variable'];
            $a = $this->_config['api_action_variable'];

            //获取模块名称
            isset($_GET[$m]) and $result['m'] = $_GET[$m];
            //获取控制器名称
            isset($_GET[$c]) and $result['c'] = $_GET[$c];
            //获取操作名称，类方法不区分大小写
            isset($_GET[$a]) and $result['a'] = $_GET[$a];
            //参数为剩余的变量
            unset($_GET[$m], $_GET[$c], $_GET[$a]);
            $result['p'] = $_GET;

            Developer::status('fetchurl_in_topspeed_end');
        } else {
            # 普通的pathinfo模式
            if ($this->_config['domain_deploy_on']) {
                $__module = $this->parseDomain() and $result['m'] = $__module;
            } else {
                $__module = null;
            }
            //检查、寻找和解析URI路由 'route_on'
            //普通模式下解析URI地址

            Developer::status('parseurl_in_common_begin');
            $ap = $this->_config['ap_bridge'];
            if (strpos($url, $tail = (string)$this->_config['masquerade_tail']) !== false) {
                $url = Chars::striptail($url, $tail);
            }

            //-- 解析PATHINFO --//
            //截取参数段param与定位段local
            $papos = strpos($url, $ap);
            $mcapart = null;
            $pparts = '';
            if (false === $papos) {
                $mcapart = trim($url, '/');//不存在参数则认定PATH_INFO全部是MCA的部分，否则得到结果substr($url,0,0)即空字符串
            } else {
                $mcapart = trim(substr($url, 0, $papos), '/');
                $pparts = substr($url, $papos + strlen($ap));
            }

            //-- 解析MCA部分 --//
            //逆向检查CA是否存在衔接
            $mcaparsed = self::parseMCA($mcapart);

            if ($__module) {
                if (is_string($__module) and strpos($__module, '/') !== false) {
                    $__module = explode('/', $__module);
                }
                $result['m'] = $mcaparsed['m'] ? array_merge($__module, $mcaparsed['m']) : $__module;
            } else {
                $result['m'] = $mcaparsed['m'];
            }
            empty($mcaparsed['c']) or $result['c'] = $mcaparsed['c'];
            empty($mcaparsed['a']) or $result['a'] = $mcaparsed['a'];

            //-- 解析参数部分 --//
            $result['p'] = self::fetchKeyValuePair($pparts);
            Developer::status('parseurl_in_common_end');

        }

        if (empty($result['m'])) {
            $result['m'] = '';
        } else {
            $result['m'] = implode('/', (array)$result['m']);
        }

        return array_values($result);
    }

    /**
     * 解析域名对应的模块
     * subdomain_mappinig如果对应着子模块，可以有下面几种写法：
     * [
     *  'www'=>'pgy/home',# 子模块之间用'/'分割开
     *  'home'=>'pgy.home', # 子模块之间用'.'分割开
     *  'home'=>['pgy','home'], # 直接解析成模块数组
     * ]
     * @return array
     */
    private function parseDomain(): array
    {
        $module = [];
        //如果绑定了模块，之后的解析将无法指定模块
        $subdomain = strstr($_SERVER['SERVER_NAME'], $this->_config['domain'], true);
        if ($subdomain !== false and $subdomain = rtrim($subdomain, '.')) {
            if (isset($this->_config['subdomain_mappinig'][$subdomain])) {
                $module = $this->_config['subdomain_mappinig'][$subdomain];
                if (is_string($module) and $module) {
                    if (strpos($module, '/') !== false) {
                        $delimiter = '/';
                    } elseif (strpos($module, '.') !== false) {
                        $delimiter = '.';
                    } else {
                        $delimiter = null;
                    }
                    $delimiter and $module = explode($delimiter, $module);
                }
            } elseif ($subdomain !== 'www' and $this->_config['subdomain_auto_mapping_on']) {
                if (false !== strpos($subdomain, '.')) {
                    $module = explode('.', $subdomain);
                } else {
                    $module = [$subdomain];
                }
            }
        }
        return $module;
    }

//----------------------------------------- create ---------------------------------//

    public function create(string $modules, string $controller, string $action, array $params = []): string
    {
        if ($this->_config['api_mode_on']) {
            $uri = self::getBasicUrl() . '?' . self::createInAPI($modules, $controller, $action, $params);
        } else {
            //反向域名地址
            $moduleUsed = false;
            if ($this->_config['domain_deploy_on']) {
                $hostname = self::createHostname($modules, $moduleUsed);//如果绑定了模块，之后的解析将无法指定模块
            } else {
                $hostname = $_SERVER['SERVER_NAME'];
            }
            $uri = self::getBasicUrl(null, $hostname) . '/' .
                self::createInCommon($moduleUsed ? null : $modules, $controller, $action, $params);
        }
        return $uri;
    }

    /**
     * $url规则如：
     *  .../Ma/Mb/Cc/Ad
     * 依次从后往前解析出操作，控制器，模块(如果存在模块将被认定为完整的模块路径)
     * @param string $url 快速创建的URL字符串
     * @param array $params GET参数数组
     * @return string
     */
    public function build($url = null, array $params = [])
    {
        //解析参数中的$url
        $params or $params = [];
        $hashpos = strpos($url, '#');
        if ($hashpos) {
            $hash = substr($url, $hashpos + 1);
            $url = substr($url, 0, $hashpos);
        } else {
            $hash = '';
        }
        $parts = @explode('/', trim($url, '/'));

        //调用URLHelper创建URL
        $action = array_pop($parts);
        $ctler = $action ? array_pop($parts) : '';
        $modules = $ctler ? $parts : '';
        $url = $this->create($modules, $ctler, $action, $params);
        return $hash ? $url : "{$url}#{$hash}";
    }

    /**
     * 按照API模式创建URL地址
     * @param array|string $modules
     * @param string $contler
     * @param string $action
     * @param array|null $params
     * @return string
     */
    private function createInAPI($modules, $contler, $action, array $params = [])
    {
        is_array($modules) and $modules = implode('/', $modules);
        return http_build_query(array_merge($params, array(
            $this->_config['api_modules_variable'] => $modules,
            $this->_config['api_controller_variable'] => $contler,
            $this->_config['api_action_variable'] => $action,
        )));
    }


    /**
     * @param null $modules
     * @param null $contler
     * @param null $action
     * @param array|null $params
     * @return string
     */
    private function createInCommon($modules = null, $contler = null, $action = null, array $params = null): string
    {
        $uri = '';
        $modules and $uri .= is_array($modules) ? implode('/', $modules) : $modules;
        $contler and $uri .= '' === $uri ? $contler : '/' . $contler;
        $action and $uri .= '/' . $action;
        $params and $uri .= $this->_config['ap_bridge'] . self::toParametersString($params);
        return $uri;
    }

    /**
     * 获取主机名称
     * @param string|array $modules
     * @param bool $flag
     * @return null|string
     */
    private function createHostname($modules, &$flag)
    {
        //模块标识符
        $mid = is_array($modules) ? implode('/', $modules) : $modules;
        $rmapping = array_flip($this->_config['subdomain_mappinig']);
        if (isset($rmapping[$mid])) {
            $hostname = $rmapping[$mid];
        } elseif ($this->_config['SUBDOMAIN_AUTO_MAPPING_ON']) {
            if (is_string($modules)) {
                $modules = strtolower(str_replace('/', '.', $modules));
            } else {
                $modules = implode('.', $modules);
            }
            $hostname = $modules;
        } else {
            return $_SERVER['SERVER_NAME'];
        }
        $flag = true;//标注模块信息已经注入到域名中了
        return $hostname . '.' . $this->_config['domain'];
    }
//----------------------------------------- 公共方法：可以供自实现使用 ---------------------------------//


    /**
     * 将参数数组转换成参数序列，应用Router模块的配置
     * @param array $params 参数数组
     * @param string $ppb
     * @param string $pkvb
     * @return string
     */
    public static function toParametersString(array $params = null, $ppb = '/', $pkvb = '/')
    {
        $temp = '';
        if ($params) {
            //参数检测
            $pkvb or $pkvb = $ppb;

            $flag = true;
            foreach ($params as $key => $val) {
                if ($flag) {
                    $temp .= "{$key}{$pkvb}{$val}";
                    $flag = false;
                } else {
                    $temp .= "{$ppb}{$key}{$pkvb}{$val}";
                }
            }
        }
        return $temp;
    }


    /**
     * 获取基础URI
     * 当端口号为80时隐藏之
     * @param string|null $hostname 主机名称
     * @param bool $full 是否取完整
     * @return string 返回URI的基础部分
     */
    private static function getBasicUrl($hostname = null, $full = false)
    {
        $uri = $full ?
            $_SERVER['REQUEST_SCHEME'] . '://' . (isset($hostname) ? $hostname : $_SERVER['SERVER_NAME']) .
            (80 == $_SERVER['SERVER_PORT'] ? '' : ':' . $_SERVER['SERVER_PORT']) . $_SERVER['SCRIPT_NAME'] :
            $_SERVER['SCRIPT_NAME'];
        return $uri;
    }

    /**
     * @param string $action
     * @param string $controller
     * @param array $modules
     * @param string $mmb
     * @param string $mcb
     * @param string $cab
     * @return string
     */
    public static function createMCA(string $action, string $controller = '', array $modules = [], $mmb = '/', $mcb = '/', $cab = '/')
    {
        $url = '';
        $url .= implode($mmb, $modules);
        if ($url) {
            $url .= $mcb;
        }
        $url .= $controller;
        if ($url) {
            $url .= $cab;
        }
        return $url . $action;
    }

    /**
     * 解析"模块、控制器、操作"
     * @param $mcapart
     * @param string $mm 模块之间的连接符号
     * @param string $mc 模块控制器之间的连接
     * @param string $ca 控制器和操作之间的连接符号
     * @return array ['m' => '模块', 'c' => '控制器', 'a' => '操作']
     */
    public static function parseMCA($mcapart, $mm = '/', $mc = '/', $ca = '/')
    {
        $parsed = ['m' => null, 'c' => null, 'a' => null];
        $mcapart = trim($mcapart, ' /');

        $capos = strrpos($mcapart, $ca);
        if (false === $capos) {
            //找不到控制器与操作之间分隔符（一定不存在控制器）
            //先判断位置部分是否为空字符串来决定是否有操作名称
            if (strlen($mcapart)) {
                //位置字段全部是字符串的部分
                $parsed['a'] = $mcapart;
            } else {
                //没有操作部分，MCA全部使用默认的
            }
        } else {
            $parsed['a'] = substr($mcapart, $capos + strlen($ca));

            //CA存在衔接符 则说明一定存在控制器
            $mcalen = strlen($mcapart);
            $mcpart = substr($mcapart, 0, $capos - $mcalen);//去除了action的部分

            if (strlen($mcapart)) {
                $mcpos = strrpos($mcpart, $mc);
                if (false === $mcpos) {
                    //不存在模块
                    if (strlen($mcpart)) {
                        //全部是控制器的部分
                        $parsed['c'] = $mcpart;
                    } else {
                        //没有控制器部分，则使用默认的
                    }
                } else {
                    //截取控制器的部分
                    $parsed['c'] = substr($mcpart, $mcpos + strlen($mc));

                    //既然存在MC衔接符 说明一定存在模块
                    $mpart = substr($mcpart, 0, $mcpos - strlen($mcpart));//以下的全是模块部分的字符串
                    if (strlen($mpart)) {
                        if (false === strpos($mpart, $mm)) {
                            $parsed['m'] = [$mpart];
                        } else {
                            $parsed['m'] = explode($mm, $mpart);
                        }
                    } else {
                        //一般存在衔接符的情况下不为空,但也考虑下特殊情况
                    }
                }
            } else {
                //一般存在衔接符的情况下不为空,但也考虑下特殊情况
            }
        }
        isset($parsed['m']) or $parsed['m'] = [];
        isset($parsed['c']) or $parsed['c'] = '';
        return $parsed;
    }


    /**
     * 将参数序列装换成参数数组，应用Router模块的配置
     * @param string $params
     * @param string $ppb
     * @param string $pkvb
     * @return array
     */
    public static function fetchKeyValuePair($params, $ppb = '/', $pkvb = '/')
    {//解析字符串成数组
        $pc = [];
        if ($ppb !== $pkvb) {//使用不同的分割符
            $params = trim($params, " {$ppb}{$pkvb}");
            $parampairs = explode($ppb, $params);
            foreach ($parampairs as $val) {
                $pos = strpos($val, $pkvb);
                if (false === $pos) {
                    //非键值对，赋值数字键
                } else {
                    $key = substr($val, 0, $pos);
                    $val = substr($val, $pos + strlen($pkvb));
                    $pc[$key] = $val;
                }
            }
        } else {//使用相同的分隔符
            $params = trim($params, " {$ppb}");
            $elements = explode($ppb, $params);
            $count = count($elements);
            for ($i = 0; $i < $count; $i += 2) {
                if (isset($elements[$i + 1])) {
                    $pc[$elements[$i]] = $elements[$i + 1];
                } else {
                    //单个将被投入匿名参数,先废弃
                }
            }
        }
        return $pc;
    }

    /**
     * 使用正则表达式匹配uri
     * @param string $pattern 路由规则
     * @param string $url 传递进来的URL字符串
     * @param array|string|callable $rule 路由导向结果
     * @return array|string|null
     */
    public static function matchRegular(string $pattern, string $url, $rule)
    {
        $result = null;
        // do the RegEx match? use '#' to ignore '/'
        if (preg_match('#^' . $pattern . '$#', $url, $matches)) {
            if (is_array($rule)) {
                $len = count($matches);
                for ($i = 1; $i < $len; $i++) {
                    $key = '$' . $i;
                    if (isset($rule['$' . $i])) {
                        $v = (string)$rule[$key];
                        if (strpos($v, '.')) {
                            $a = explode('.', $v);
                            empty($rule[$a[0]]) and $rule[$a[0]] = [];
                            $rule[$a[0]][$a[1]] = $matches[$i];
                        } else {
                            $rule[$v] = $matches[$i];
                        }
                    } else {
                        empty($rule['o']) and $rule['o'] = [];
                        $rule['o'][] = $matches[$i];
                    }
                    unset($rule[$key]);
                }
                $result = $rule;
            } elseif (is_string($rule)) {
                $result = preg_replace('#^' . $pattern . '$#', $rule, $url);//参数一代表的正则表达式从参数三的字符串中寻找匹配并替换到参数二代表的字符串中
            } elseif (is_callable($rule)) {
                array_shift($matches);
                // Execute the callback using the values in matches as its parameters.
                $result = call_user_func_array($rule, $matches);//参数二是完整的匹配
                if ($result === true) {
                    //返回true表示直接完成
                    exit();
                } elseif (!is_string($result) and !is_array($result)) {
                    //要求结果必须返回string或者数组
                    return null;
                }
            }
        }
        return $result;
    }


    /**
     * 解析路径 "模块s/控制器/操作"
     * @param string $path
     * @return array [module,controller,action]
     */
    public static function parsePath(string $path): array
    {
        $pos = strpos($path, '?');
        if ($pos !== false) {
            parse_str(substr($path, $pos, -1), $query);
            $path = substr($path, 0, $pos);
        }
        $path = explode('/', trim($path, '/'));
        $action = array_pop($path);
        $controller = array_pop($path);
        $modules = implode('/', $path);
        return [$modules, $controller??'', $action ?? ''];
    }
}